﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using CokoNES.Emulator.Hardware;
using System.IO;
using CokoNES.Tools;

namespace CokoNES.Emulator
{
    public class Nes
    {
        public enum SystemState { Stoped, Running, Paused }

        private SystemState state;
        private Cpu6502 mainProcessor;
        private Cartridge myCartridge;
        
        public Nes()
        {
            this.mainProcessor = new Cpu6502();

            this.state = SystemState.Stoped;
        }

        /// <summary>
        /// Byte     Contents
        /// ---------------------------------------------------------------------------
        /// 0-3      String "NES^Z" used to recognize .NES files.
        /// 4        Number of 16kB ROM banks.
        /// 5        Number of 8kB VROM banks.
        /// 6        bit 0     1 for vertical mirroring, 0 for horizontal mirroring.
        ///          bit 1     1 for battery-backed RAM at $6000-$7FFF.
        ///          bit 2     1 for a 512-byte trainer at $7000-$71FF.
        ///          bit 3     1 for a four-screen VRAM layout. 
        ///          bit 4-7   Four lower bits of ROM Mapper Type.
        /// 7        bit 0     1 for VS-System cartridges.
        ///          bit 1-3   Reserved, must be zeroes!
        ///          bit 4-7   Four higher bits of ROM Mapper Type.
        /// 8        Number of 8kB RAM banks. For compatibility with the previous
        ///          versions of the .NES format, assume 1x8kB RAM page when this
        ///          byte is zero.
        /// 9        bit 0     1 for PAL cartridges, otherwise assume NTSC.
        ///          bit 1-7   Reserved, must be zeroes!
        /// 10-15    Reserved, must be zeroes!
        /// 16-...   ROM banks, in ascending order. If a trainer is present, its
        ///          512 bytes precede the ROM bank contents.
        /// ...-EOF  VROM banks, in ascending order.
        /// ---------------------------------------------------------------------------
        /// </summary>
        /// <param name="romPath">Rom path</param>
        /// <returns>True if the cartridge is correctly loaded, else false</returns>
        public bool LoadRom(string romPath)
        {
            var loadingState = false;

            using (var reader = File.OpenRead(romPath))
            {
                var myCartridge = new Cartridge();
                reader.Read(myCartridge.header, 0, 16);

                // Check if the cartridge look's valid
                if (ASCIIEncoding.ASCII.GetString(myCartridge.header.Take(3).ToArray<byte>()) == "NES")
                {
                    #region Load header

                    myCartridge.PRGROM_Size = myCartridge.header[4];
                    myCartridge.CHRROM_Size = myCartridge.header[5];

                    myCartridge.Flag6 = myCartridge.header[6];
                    myCartridge.Flag7 = myCartridge.header[7];

                    myCartridge.PRGRAM_Size = myCartridge.header[8];

                    myCartridge.Flag9 = myCartridge.header[9];
                    myCartridge.Flag10 = myCartridge.header[10];

                    switch (myCartridge.Flag6 & 0x09)
                    {
                        case 0x0: myCartridge.mirroringMod = Cartridge.Mirroring.Horizontal; break;
                        case 0x1: myCartridge.mirroringMod = Cartridge.Mirroring.Vertical; break;
                        case 0x8:
                        case 0x9: myCartridge.mirroringMod = Cartridge.Mirroring.FourScreen; break;
                    }

                    myCartridge.batteryAvailable = (myCartridge.Flag6 & 0x2) == 0x2;
                    myCartridge.trainerAvailable = (myCartridge.Flag6 & 0x4) == 0x4;

                    // TODO Need a test !
                    myCartridge.mapper = (byte)((myCartridge.Flag7 & 0xF0) + ((myCartridge.Flag6 & 0xF0)));

                    myCartridge.isVsRom = (myCartridge.Flag7 & 0x1) == 0x1;
                    myCartridge.isPlayerChoice = (myCartridge.Flag7 & 0x2) == 0x2;
                    myCartridge.isNes2Format = ((myCartridge.Flag7 & 0xC) >> 2) == 0x2;
                    
                    #endregion

                    #region Load rom

                    if (myCartridge.trainerAvailable)
                    {
                        myCartridge.Trainer_Data = new byte[512];
                        reader.Read(myCartridge.Trainer_Data, 0 , 512);
                    }

                    myCartridge.PRGROM_Data = new byte[myCartridge.PRGROM_Size][];

                    for (int i = 0; i < myCartridge.PRGROM_Size; i++)
                    {
                        myCartridge.PRGROM_Data[i] = new byte[16384];
                        reader.Read(myCartridge.PRGROM_Data[i], 0, 16384);
                    }

                    if (myCartridge.CHRROM_Size > 0)
                    {
                        myCartridge.CHRROM_Data = new byte[myCartridge.CHRROM_Size][];

                        for (int i = 0; i < myCartridge.CHRROM_Size; i++)
                        {
                            myCartridge.CHRROM_Data[i] = new byte[8192];
                            reader.Read(myCartridge.CHRROM_Data[i], 0, 8192);
                        }
                    }
                    else
                    {
                        myCartridge.CHRROM_Data = new byte[0][];
                        myCartridge.CHRROM_Data[0] = new byte[8192];
                    }

                    if (myCartridge.isPlayerChoice)
                    {
                        myCartridge.PlayChoice_Data = new byte[8192];
                        reader.Read(myCartridge.PlayChoice_Data, 0, 8192);
                    }

                    #endregion

                    #if DEBUG

                    Console.WriteLine(string.Format("######### Cartridge loaded #########"));
                    Console.WriteLine(string.Format("# PRGROM Size : {0}, CHRROM Size : {1}, PRGRAM Size : {2}", myCartridge.PRGROM_Size, myCartridge.CHRROM_Size, myCartridge.PRGRAM_Size));

                    switch (myCartridge.mirroringMod)
                    {
                        case Cartridge.Mirroring.Horizontal: Console.WriteLine(string.Format("# Mirroring mod : Horizontal")); break;
                        case Cartridge.Mirroring.Vertical: Console.WriteLine(string.Format("# Mirroring mod : Vertical")); break;
                        case Cartridge.Mirroring.FourScreen: Console.WriteLine(string.Format("# Mirroring mod : 4Screen")); break;
                    }

                    Console.WriteLine(string.Format("# Trainer available : {0}, Battery available : {1}, Mapper number : {2}", myCartridge.trainerAvailable, myCartridge.batteryAvailable, myCartridge.mapper));
                    Console.WriteLine(string.Format("# Is PlayerChoice ? : {0}, Is VS System rom ? : {1}, Is NES 2.0 Header ? : {2}", myCartridge.isPlayerChoice, myCartridge.isVsRom, myCartridge.isNes2Format));
                    Console.WriteLine(string.Format("####################################"));

                    #endif

                    this.myCartridge = myCartridge;

                    loadingState = true;
                }
            }

            return loadingState;
        }

        public void Reset()
        {
            // TODO Reset the system
        }
        
        public void Run()
        {
            Reset();

            this.state = SystemState.Running;

            // Loading PRGROM into the main RAM
            var prgromsize = myCartridge.PRGROM_Size > 2 ? 2 : 1;

            for (int i = 0; i < prgromsize; i++)
            {
                Memory.MemsetAdv(myCartridge.PRGROM_Data[i], RAM.R, 0, 0x8000, 0x4000);
            }

            this.mainProcessor.PC = (byte)(RAM.R[0xFFFC] + (RAM.R[0xFFFD] << 8));

            // TODO Create the dispatcher here
            while (this.state == SystemState.Running)
            {
                byte opcode = RAM.R[this.mainProcessor.PC];

                switch (opcode)
                {
                    case 0x69: mainProcessor.Opcode_ADC(adressing: Cpu6502.AddressingMod.Immediate); break;
                    case 0x65: mainProcessor.Opcode_ADC(adressing: Cpu6502.AddressingMod.ZeroPage); break;
                    case 0x75: mainProcessor.Opcode_ADC(adressing: Cpu6502.AddressingMod.IndexedZeroPageX); break;
                    case 0x60: mainProcessor.Opcode_ADC(adressing: Cpu6502.AddressingMod.Absolute); break;
                    case 0x70: mainProcessor.Opcode_ADC(adressing: Cpu6502.AddressingMod.AbsoluteIndexedX); break;
                    case 0x79: mainProcessor.Opcode_ADC(adressing: Cpu6502.AddressingMod.AbsoluteIndexedY); break;
                    case 0x61: mainProcessor.Opcode_ADC(adressing: Cpu6502.AddressingMod.IndexedIndirectX); break;
                    case 0x71: mainProcessor.Opcode_ADC(adressing: Cpu6502.AddressingMod.IndirectIndexedX); break;
                }

                #if DEBUG

                Console.WriteLine("{0}: Opcode {1}", this.mainProcessor.PC.ToString("X2"), opcode.ToString("X"));

                #endif

                this.mainProcessor.PC++;
            }
        }
    }
}
